spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import { GitCompareArrows } from 'lucide-react';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { Suspense } from 'react';5import { Bars } from '@/components/charts/bars';6import { LineChart } from '@/components/charts/line-chart';7import { WorldMap } from '@/components/charts/world-map';8import { CompanyHeader, DensityStrip } from '@/components/company/company-header';9import { CompanyMiniList } from '@/components/company/company-table';10import { MetricTiles } from '@/components/company/metric-tiles';11import { ConfidenceLegend, HistoryPanel, JobsPanel, LocationsPanel, PeoplePanel, PricingPanel, ProductsPanel, SensorsTable, SignalsPanel, TimelinePanel } from '@/components/company/panels';12import { CorporateStructurePanel, KeyFactsPanel } from '@/components/company/profile-panels';13import { EventList } from '@/components/events/event-row';14import { Chip } from '@/components/ui/badges';15import { KV, Row } from '@/components/ui/key-value';16import { Container, Empty, Note } from '@/components/ui/section';17import { TabPanel, Tabs } from '@/components/ui/tabs';18import { api, ApiError, safe } from '@/lib/api';19import { fmtDate, fmtPct, humanize } from '@/lib/format';20import { bool, str, type SP } from '@/lib/params';21import { sourceFor } from '@/lib/profile';22import { routes, SURFACE_LABELS } from '@/lib/site';23import type { CompanyDetail, MapBucket } from '@/lib/types';2425export const revalidate = 120;2627const TABS = [28 { id: 'overview', label: 'Overview' },29 { id: 'timeline', label: 'Timeline' },30 { id: 'signals', label: 'Signals' },31 { id: 'jobs', label: 'Jobs' },32 { id: 'products', label: 'Products' },33 { id: 'pricing', label: 'Pricing' },34 { id: 'locations', label: 'Locations' },35 { id: 'leadership', label: 'Leadership' },36 { id: 'sources', label: 'Sources' },37 { id: 'history', label: 'History' },38];3940export default async function CompanyPage({ params, searchParams }: { params: Promise<{ slug: string }>; searchParams: Promise<SP> }) {41 const { slug } = await params;42 const sp = await searchParams;43 let c: CompanyDetail;44 try {45 c = await api.company(slug);46 } catch (e) {47 if (e instanceof ApiError && e.notFound) notFound();48 throw e;49 }50 const requested = str(sp.tab) ?? 'overview';51 const tab = TABS.some((t) => t.id === requested) ? requested : 'overview';52 const tabs = TABS.map((t) => ({ ...t, count: t.id === 'jobs' ? c.counts.jobs_open : t.id === 'sources' ? c.counts.sensors : t.id === 'signals' ? c.signals?.length || undefined : undefined }));53 // Products tab fallback: Wikidata "product or material produced" statements, with the provenance of that field.54 const wdProducts = c.profile?.products ?? [];55 const wdSrc = sourceFor(c.profile, 'products');56 const wdSource = wdSrc ? { url: wdSrc.url, retrieved_at: wdSrc.retrieved_at } : c.profile?.wikidata_url ? { url: c.profile.wikidata_url, retrieved_at: c.profile.enriched_at ?? '' } : null;5758 return (59 <Container wide>60 <CompanyHeader c={c} />61 <MetricTiles c={c} />62 <DensityStrip c={c} />63 <Suspense>64 <Tabs tabs={tabs} className="mt-4" sticky>65 <TabPanel id={tab}>66 {tab === 'overview' && <Overview c={c} />}67 {tab === 'timeline' && <TimelinePanel slug={c.slug} data={await safe(api.companyTimeline(c.slug, str(sp.filter) ?? 'all'))} filter={str(sp.filter) ?? 'all'} />}68 {tab === 'signals' && <SignalsPanel signals={c.signals ?? []} />}69 {tab === 'jobs' && <JobsPanel slug={c.slug} data={await safe(api.companyJobs(c.slug, { status: str(sp.status) ?? 'open', ai: bool(sp.ai) ? 1 : undefined, per_page: 50, page: str(sp.page) }))} status={str(sp.status) ?? 'open'} ai={bool(sp.ai)} />}70 {tab === 'products' && <ProductsPanel data={await safe(api.companyProducts(c.slug))} wikidataProducts={wdProducts} wikidataSource={wdSource} />}71 {tab === 'pricing' && <PricingPanel data={await safe(api.companyPricing(c.slug))} />}72 {tab === 'locations' && <Locations slug={c.slug} />}73 {tab === 'leadership' && <PeoplePanel data={await safe(api.companyPeople(c.slug))} />}74 {tab === 'sources' && <Sources c={c} />}75 {tab === 'history' && <HistoryPanel data={await safe(api.companyHistory(c.slug))} />}76 </TabPanel>77 </Tabs>78 </Suspense>79 <div className="mt-10 flex flex-wrap items-center justify-between gap-3 border-t border-rule pt-5">80 <p className="text-sm text-ink-2">Compare {c.display_name} with peers on activity, hiring, product velocity, AI adoption, locations and events.</p>81 <Link href={routes.compare([c.slug])} className="btn">82 <GitCompareArrows className="size-4" aria-hidden /> Compare companies83 </Link>84 </div>85 </Container>86 );87}8889async function Overview({ c }: { c: CompanyDetail }) {90 const [events, similar, metrics] = await Promise.all([safe(api.companyEvents(c.slug, { per_page: 10 })), safe(api.companySimilar(c.slug, 8)), safe(api.companyMetrics(c.slug, 90))]);91 const surfaces = Object.entries(c.sensors_by_surface ?? {}).sort((a, b) => b[1] - a[1]);92 const series = metrics?.series ?? {};93 const lines = (['activity_score', 'product_velocity', 'ai_adoption'] as const).filter((k) => (series[k]?.length ?? 0) > 1).map((k) => ({ id: k, label: humanize(k), points: series[k]!.map((p) => ({ day: p.day, value: p.value })) }));94 return (95 <div className="grid gap-8 lg:grid-cols-12">96 <div className="space-y-8 lg:col-span-8">97 <KeyFactsPanel c={c} />98 <section>99 <p className="eyebrow mb-2">Latest structured events</p>100 {events ? <EventList events={events.items} variant="table" showCompany={false} emptyLabel="No structured events detected for this company yet — sensors are attached and observing." /> : <Empty title="Events temporarily unavailable." />}101 {events && events.total > 10 && (102 <p className="mt-2 text-sm">103 <Link href={routes.company(c.slug, 'timeline')} className="link">104 Full timeline ({events.total} events) →105 </Link>106 </p>107 )}108 </section>109 <section>110 <p className="eyebrow mb-2">90-day metric series</p>111 {lines.length ? <LineChart series={lines} height={200} yZero /> : <Empty compact title="Not enough history for a series yet." />}112 </section>113 {c.signals?.length > 0 && (114 <section>115 <p className="eyebrow mb-2">Active signals</p>116 <SignalsPanel signals={c.signals.slice(0, 3)} />117 </section>118 )}119 </div>120 <aside className="space-y-8 lg:col-span-4">121 <section>122 <p className="eyebrow mb-2">Identity</p>123 <KV>124 <Row k="Legal name">{c.legal_name ?? '—'}</Row>125 <Row k="Domains">126 {c.domains?.length ? (127 <span className="mono text-xs">128 {c.domains.map((d) => (129 <span key={d.domain} className="block">130 {d.domain} <span className="text-ink-3">· {d.kind}</span>131 </span>132 ))}133 </span>134 ) : (135 <span className="mono text-xs">{c.canonical_domain}</span>136 )}137 </Row>138 {c.aliases?.length > 0 && <Row k="Aliases">{c.aliases.join(', ')}</Row>}139 <Row k="Status">140 <Chip>{c.status.toLowerCase().replace(/_/g, ' ')}</Chip>141 </Row>142 <Row k="Importance">143 <span className="tnum">144 {c.importance} · tier {c.tier}145 </span>146 </Row>147 <Row k="Coverage">148 <span className="tnum">149 {fmtPct(c.coverage?.historical_coverage, 0)} · {c.coverage?.days_observed ?? '—'} days observed150 {c.coverage?.first_observed_at ? ` · since ${fmtDate(c.coverage.first_observed_at)}` : ''}151 </span>152 </Row>153 {c.coverage?.sensor_uptime !== null && c.coverage?.sensor_uptime !== undefined && (154 <Row k="Sensor uptime">155 <span className="tnum">{fmtPct(c.coverage.sensor_uptime, 1)}</span>156 </Row>157 )}158 </KV>159 </section>160 <CorporateStructurePanel relationships={c.relationships ?? []} />161 <section>162 <p className="eyebrow mb-2">Sensors by surface</p>163 {surfaces.length ? <Bars dense rows={surfaces.map(([s, n]) => ({ key: s, label: SURFACE_LABELS[s] ?? s, value: n }))} /> : <Empty compact />}164 <p className="mt-1 text-xs">165 <Link href={routes.company(c.slug, 'sources')} className="link">166 All sensors →167 </Link>168 </p>169 </section>170 <section>171 <p className="eyebrow mb-2">Similar companies</p>172 {similar ? <CompanyMiniList items={similar.items} metric="activity_score" label="activity" /> : <Empty compact />}173 </section>174 <ConfidenceLegend />175 </aside>176 </div>177 );178}179180async function Locations({ slug }: { slug: string }) {181 const data = await safe(api.companyLocations(slug));182 const buckets: MapBucket[] = (data?.items ?? []).filter((l) => l.status === 'listed' && l.lat !== null && l.lon !== null).map((l) => ({ lat: l.lat as number, lon: l.lon as number, country: l.country ?? '', city: l.city, companies: 1, events_30d: 1, jobs_open: 0, top: [] }));183 return <LocationsPanel data={data} map={buckets.length ? <WorldMap buckets={buckets} metric="companies" interactive={false} highlight={[...new Set(buckets.map((b) => b.country))]} /> : <Note>No geocoded locations to draw yet.</Note>} />;184}185186async function Sources({ c }: { c: CompanyDetail }) {187 const data = await safe(api.companySensors(c.slug));188 if (!data) return <Empty title="Sensors temporarily unavailable." />;189 const items = [...data.items].sort((a, b) => (a.status === b.status ? b.event_count - a.event_count : a.status === 'active' ? -1 : 1));190 return (191 <div className="space-y-3">192 <Note>193 Every fact on this profile traces back to one of these sensors. A sensor is a deployed connector watching one public URL on a schedule (tier A = 5–15 min … E = 3–7 days). Open a sensor for its observations, snapshots and changes.194 </Note>195 <SensorsTable items={items} />196 </div>197 );198}199